package org.malajava.shop.util;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

/**
 * 2018年4月25日，修改 load 方法中中 加载驱动的代码
 */
public final class JdbcHelper {
	
	private static final String CONNECT_DATABASE = "jdbc.connection" ;
	private static final String PREFIX = "jdbc.connection." ;
	private static final String DRIVER_SUFFIX = ".driver" ;
	private static final String URL_SUFFIX = ".url" ;
	private static final String USER_SUFFIX = ".user" ;
	private static final String PASSWORD_SUFFIX = ".password" ;
	private static final String AUTOCOMMIT_SUFFIX =".autocommit" ;
	private static final String ISOLATION_SUFFIX =".isolation" ;
	
	// 声明一个类字段用来记录驱动的加载状态
	private static boolean unload  = true ; // 约定 true 表示没有加载，false 表示已经加载
	
	private String database ; // 用来存储所连接的数据库名称的字段

	private String driver; // 存储数据库驱动类名称的字段
	private String url ; // 存储数据库连接的字段
	private String username ; // 存储数据库用户名
	private String password; // 存储数据库密码
	private boolean autocommit ; // 存储事务提交模式的状态( true 表示自动提交，false表示手动提交 )
	private int isolation ; // 存储事务隔离级别( 取值可以是 0、1、2、4、8 )
	
	private  ClassLoader loader;
	
	private Connection connection ; // 用来缓存已经创建好的数据库连接
	
	public JdbcHelper() {
		this("config.properties"); //调用本类的另外一个构造方法
	}
	
	public JdbcHelper( String path ) {
		// Thread.currentThread() 获得当前线程
		// Thread.currentThread().getContextClassLoader()  获得当当前线程使用的类加载器
		this.loader = Thread.currentThread().getContextClassLoader() ;
		this.init( path );
	}
	
	/**
	 * 读取类路径下的指定文件
	 * @param path 类路径下的文件
	 */
	private void init( String path ) {
		if( path == null ) {
			throw new RuntimeException( "未指定数据库配置文件" );
		}
		
		// 如果 path 是以 / 开头的
		if( path.startsWith( "/" ) ) {
			path = path.substring( 1 ) ; // 舍弃第一个 /
		}
		
		// 使用类加载器加载 类路径 下的配置文件
		InputStream in = loader.getResourceAsStream( path );
		Properties props = new Properties();
		try {
			props.load( in );
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		database = props.getProperty( CONNECT_DATABASE ) ; 
		
		String driverKey = PREFIX + database + DRIVER_SUFFIX ;
		driver = props.getProperty( driverKey ) ; 
		
		String urlKey = PREFIX + database + URL_SUFFIX ;
		url = props.getProperty( urlKey ) ; 
		
		String userKey = PREFIX + database + USER_SUFFIX ;
		username = props.getProperty( userKey ) ; 
		
		String passwordKey = PREFIX + database + PASSWORD_SUFFIX ;
		password = props.getProperty( passwordKey ) ; 
		
		String autocommitKey = PREFIX + database + AUTOCOMMIT_SUFFIX ;
		autocommit = Boolean.parseBoolean( props.getProperty( autocommitKey ) ) ; 
		
		String isolationKey = PREFIX + database + ISOLATION_SUFFIX ;
		isolation = Integer.parseInt( props.getProperty( isolationKey ) ) ;
		
	}
	
	/** 加载数据库驱动 */
	private void load() {
		if( unload ) { // 如果驱动还没有加载
			try {
				// Class.forName(driver); //加载驱动
				Class.forName( driver , true , loader ) ;
				unload = false ; // 修改 unload 的状态
			} catch (ClassNotFoundException e) {
				System.out.println("驱动[ " + driver + " ]加载失败");
				e.printStackTrace();
			}
		}
	}
	
	/** 获取数据库连接 */
	private void connect() {
		if(  this.invalid() ) { // 如果检查连接后，发现连接是无效的
			try {
				// 就返回新的连接
				connection = DriverManager.getConnection(  url , username , password) ;
				// 在新连接执行操作之前就设置 事务自动提交状态 和 事务隔离级别
				this.setTransaction();
			} catch (SQLException cause ) {
				throw new RuntimeException("数据库连接失败", cause); // 异常转译
			}
		}
	}
	
	/*** 设置事务提交状态和事务隔离级别  */
	private void setTransaction() {
		
		try {
			connection.setAutoCommit( this.autocommit );
		} catch (SQLException cause) {
			throw new RuntimeException("设置事务提交模式时发生错误", cause); // 异常转译
		}
		
		if( isolation == 0 || isolation == 1 || isolation == 2 || isolation == 4 || isolation == 8 ) {
			try {
				connection.setTransactionIsolation( this.isolation );
			} catch (SQLException cause) {
				throw new RuntimeException("设置事务隔离级别失败", cause); // 异常转译
			}
		} else {
			throw new RuntimeException( "配置文件中的事务隔离级别不是SQL92标准中指定的隔离级别" ); 
		}
		
	}
	
	/**
	 * 根据给定的SQL语句创建一个 PreparedStatement 对象，该对象可以根据 第二个参数来确定是否需要返回由数据库产生的键
	 * @param SQL 被执行的SQL语句(可以是参数化SQL)
	 * @param generated 指示是否需要获取由数据库产生的键，true表示需要获取，false表示不需要获取
	 * @return 返回 PreparedStatement 对象
	 */
	private PreparedStatement prepare( String SQL , boolean generated ) {
		
		this.load(); // 加载驱动
		this.connect(); // 获得数据库连接
		
		int autoGeneratedKeys = generated ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS ;
		
		try {
			// 第二个参数，设置是否需要获取由数据库产生的键 ，Statement.RETURN_GENERATED_KEYS 表示需要获取由数据库产生的键
			return connection.prepareStatement(SQL ,  autoGeneratedKeys );
		} catch (SQLException cause) {
			throw new RuntimeException("创建 PreparedStatement 失败", cause ); // 异常转译
		}
		
	}

	/**
	 * 专门用来执行 update 、delete 、 insert 语句的方法。
	 * 当执行 insert 语句时，如果由数据库产生了主键，则该方法不能返回由数据库产生的主键
	 * @param SQL 带有参数占位符的SQL语句( 参数化SQL )
	 * @param params 当被执行的SQL语句含有 参数占位符时，请依次传入参数占位符的取值
	 * @return 返回受当前SQL影响的记录数
	 */
	public int update( String SQL , Object... params ) {
		
		PreparedStatement ps = prepare( SQL , false );
		
		setParameters( ps , params ) ; 
		
		try {
			return ps.executeUpdate();
		} catch (SQLException cause) {
			throw new RuntimeException("SQL执行失败", cause); // 异常转译
		} finally {
			// 释放 PreparedStatement 所占用的资源
			this.close( ps );
		}
	}
	
	/**
	 * 执行插入语句，并可以获取由数据库产生的键
	 * @param SQL 需要执行的仅插入一行数据的SQL语句
	 * @param params 被执行的插入语句中如果有参数占位符，则依次指定这些参数值
	 * @return 返回由数据库产生的键，如果数据库未产生则返回 -1 
	 */
	public int insert( String SQL , Object... params ) {
		// 加载驱动、获得连接后，创建 PreparedStatement 对象 ( 可以获得由数据库产生的键 )
		PreparedStatement ps = prepare( SQL , true );
		// 设置参数占位符的值
		setParameters( ps , params ) ; 
		
		try {
			ps.executeUpdate();
		} catch (SQLException cause) {
			this.close( ps ); // 释放资源
			throw new RuntimeException("SQL执行失败", cause); // 异常转译
		}
		
		int id = -1 ;
		
		ResultSet rs = null ;
		
		try {
			// 从 PreparedStatement 对象中获取由数据库产生的主键
			// 这个结果集只有一个列组成，它可能有多行(批量插入)，也可能是单行(仅插入一行数据)
			// 这个单独的列名称是 GENERATED_KEY ，类型是 int 或 long
			rs = ps.getGeneratedKeys();
			if( rs.next() ) {
				id = rs.getInt( 1 ) ;
			}
		} catch (SQLException cause) {
			throw new RuntimeException("SQL执行失败", cause); // 异常转译
		} finally {
			// 释放资源
			this.close( rs );
			this.close( ps );
		}
		
		return id ;
		
	}
	
	/**
	 * 专门用来执行查询语句
	 * @param SQL 被执行的查询语句
	 * @param params 如果 被执行SQL中含有参数占位符，则依次传入这些参数值
	 * @return 返回根据查询结果ResultSet对象 转换而成的List集合，这个List集合中存放的是 Map 对象，每个Map 对象对应ResultSet中的一行数据。
	 *               另外，在Map对象中，每个 key 都是 该列的列名 或 列的别名。
	 */
	public List< Map<String,Object> > query( String SQL , Object... params ) {
		PreparedStatement ps = prepare( SQL , false );
		setParameters( ps , params ) ; 
		
		ResultSet rs = null ;
		try {
			rs = ps.executeQuery();
		}catch (SQLException cause) {
			throw new RuntimeException("SQL执行失败", cause); // 异常转译
		} 
		
		List<Map<String, Object>> list = convert(rs);
		
		this.close( rs );
		this.close( ps );
		
		return list ;
	}

	/***
	 * 用来将 结果集 对象转换为 List 集合的专用方法
	 * @param rs 被转换的 结果集 对象
	 * @return 返回一个List集合，其中存放的是 Map 对象，而 Map内部存放原来 结果集中的一行数据，key 是原来结果集中的列名或别名，value是结果集中某个具体行的某个列的值
	 */
	private List<Map<String, Object>> convert(ResultSet rs) {
		if( rs == null ) {
			return null ;
		}
		
		// 有一个List，它内部存放的是 Map 对象
		// 每个 Map 的 key 是 String 类型 ，value 是 Object 类型
		List< Map<String,Object> > list  = new ArrayList<>();
		
		try {
	
			// 获得结果集元数据
			final ResultSetMetaData rsmd = rs.getMetaData();
			// 返回当前的查询结果中所包含的列数
			final int count = rsmd.getColumnCount();
			
			while( rs.next() ) {
				// 创建Map集合
				Map<String,Object> map = new HashMap<>();
				// 通过循环依次处理当前行的每一列数据
				for( int i = 1 ; i <= count ; i++ ) {
					String key = rsmd.getColumnLabel( i ) ; // 以第i列的列名或列的别名为 key
					Object value = rs.getObject( i ) ; // 以当前行第i列的取值为 value
					// 将 "列名(列别名) - 当前列取值" 的键值对放入到 map 集合中
					map.put(key, value) ;
				} // 当 for 循环执行完毕后，当前行的所有数据都将添加到 map 集合中
				
				// 将当前行对应的 map 添加到 List 集合中
				list.add( map ) ;
			}
		} catch( SQLException e ) {
			e.printStackTrace();
		}
 		return list;
	}
	
	/**
	 * 为 指定的 PreparedStatement 对象设置参数占位符的取值
	 * @param ps
	 * @param params
	 */
	private void setParameters( PreparedStatement ps , Object...  params ) {
		if(  params != null && params.length > 0 ) {
			try {
				for( int i = 0 ; i < params.length ; i++ ) {
					ps.setObject( i + 1 ,  params[ i ] );
				}
			} catch( SQLException cause ) {
				throw new RuntimeException("设置参数化SQL的参数占位符取值时发生错误", cause); 
			}
		}
		
	}
	
	/** 当 connection 字段对应的连接不能使用时返回 true*/
	private boolean invalid() {
		try {
			// connection.isValid( 3 ) 判断当前连接是否有效，如果有效就返回 true
			return connection == null || connection.isValid(3 ) == false ;
		} catch (SQLException cause) {
			throw new RuntimeException("判断数据库连接状态时发生错误", cause); // 异常转译
		}
	}
	
	/** 用来关闭资源的方法，比如关闭 ResultSet、Statement、Connection */
	private void close( AutoCloseable ac ) {
		if( ac != null ) {
			try {
				ac.close();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}
	
	/** 销毁当前对象所占用的资源 */
	public void destory() {
		this.close( connection );
	}
	
	/**
	 * 专门用来 "遍历" 结果集对象的方法
	 * @param rs
	 */
	@Deprecated
	public void show(ResultSet rs) {

		if (rs == null) {
			System.out.println("你丫给的结果集是 null");
			return;
		}

		try {
			// 获得结果集元数据
			final ResultSetMetaData rsmd = rs.getMetaData();
			// 返回当前的查询结果中所包含的列数
			final int count = rsmd.getColumnCount();

			for (int i = 1; i <= count; i++) {
				System.out.print(rsmd.getColumnLabel(i) + "\t");
			}
			System.out.println();

			int rows = 0 ;
			while (rs.next()) {

				for (int i = 1; i <= count; i++) {
					System.out.print(rs.getObject(i) + "\t"); // 获得 第 i 列的值
				}
				System.out.println();

				rows++ ;
			}
			
			System.out.println( "已选择"+ rows + "行" );
			
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
}
